package com.atguigu.gmall.gateway.filter;

import com.atguigu.gmall.common.constant.RedisConstant;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.gateway.properties.AuthUrlProperties;
import com.atguigu.gmall.gateway.service.UserAuthService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * 所有请求来，都会先过过滤器
 * Fi
 */
@Component
public class UserAuthFilter implements GlobalFilter {


    @Autowired
    UserAuthService userAuthService;

    @Autowired
    AuthUrlProperties properties;

    //ant路径匹配器
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    ObjectMapper objectMapper = new ObjectMapper();

    /**
     * @param exchange 封装了请求和响应
     * @param chain    过滤器链
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //TODO 被打回登录页以后也要有 originUrl 这个路径

        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value(); // /css/index.css
        System.out.println("过滤器工作......." + path);

        //正常：无需登录的直接放行。需要登录的验证token，无权访问的直接给用户响应错误页
        //1】、任何情况都不能访问的页； 如果访问了，就响应错误json
        List<String> denyUrls = properties.getDenyUrls();
        for (String url : denyUrls) {
            //- /api/inner/**  判断是否拒绝访问的指定路径
            boolean match = antPathMatcher.match(url, path);
            if (match) {
                //如果匹配，说明是拒绝访问的 //
                Result<String> result = Result.build("", ResultCodeEnum.PERMISSION);
                //响应json数据
                return responseJson(exchange, result);
            }
        }


        //2】、任何情况都能直接访问的路径； 直接放行
        List<String> anyoneUrls = properties.getAnyoneUrls();
        for (String anyoneUrl : anyoneUrls) {
            boolean match = antPathMatcher.match(anyoneUrl, path);
            if(match){
                //不用登录就能访问
                return userIdAndTempIdThroughRequest(exchange,chain,null);
            }
        }


        //3】、必须登录才能访问
        List<String> loginUrls = properties.getLoginUrls();
        for (String loginUrl : loginUrls) {
            boolean match = antPathMatcher.match(loginUrl, path);
            if(match){
                String token = getToken(request);
                if(StringUtils.isEmpty(token)){
                    //没登录，跳去登录
                    return locationToPage(exchange,properties.getLoginPage());
                }else {
                    Long userId = userAuthService.checkToken(token);
                    if(userId == null){
                        //假登陆
                        return locationToPage(exchange,properties.getLoginPage());
                    }else {
                        //真登录了，userId透传下去
                        return  userIdAndTempIdThroughRequest(exchange,chain,userId);
                    }
                }
            }
        }



        //没说这个路径，需要登录、不需要登录、不能访问。。。。

        //需求：如果当前请求，Cookie 字段携带了 token,网关就校验一下，顺便查出这个用户对应的userId往下透传

        //1、先获取请求头字段 token 的 cookie 值，前端也有可能直接在请求头中有一个token的header
        String token = getToken(request);
        if (StringUtils.isEmpty(token)) {
            //前端没有任何登录想法，直接放行
            return userIdAndTempIdThroughRequest(exchange,chain,null);
        } else {
            //前端带了token，必须验证是否合法；只要userId不为null，代表去redis中查到了这个token对应的用户
            //TODO 循环重定向问题. 命令浏览器移除这个假cookie
            Long userId = userAuthService.checkToken(token);
            if (userId == null) {
                //说明用户带了错误令牌。把用户引导到登录页重新登录
                return locationToPage(exchange, properties.getLoginPage());

            }else {
                //带了token，而且对的，往下透传用户id
                return userIdAndTempIdThroughRequest(exchange,chain,userId);
            }

        }

    }

    /**
     * 所有的放行，都要做用户id透传和临时id透传
     * @param exchange
     * @param chain
     * @param userId
     * @return
     */
    private Mono<Void> userIdAndTempIdThroughRequest(ServerWebExchange exchange, GatewayFilterChain chain, Long userId) {
        ServerHttpRequest request = exchange.getRequest();
        //1、改请求，给请求添加上userId这个头
//        request.getHeaders().add(RedisConstant.USER_ID_REQUEST_HEADER,userId.toString());
        //1、把原exchange（req,resp）的原请求 request 复制一个出来。并修改请求头
        //2、把临时id页也透传
//        String tempId = request.getHeaders().getFirst("userTempId");
        String cookieUserTempId = getCookieValue(request, "userTempId");
        //临时id，前端有可能还会发送ajax的时候带到头上。
        String tempId = StringUtils.isEmpty(cookieUserTempId)?getHeaderValue(request,"userTempId"):cookieUserTempId;


        ServerHttpRequest.Builder reqMutate = request.mutate();

        if(userId != null){
            //用户id透传
            reqMutate.header(RedisConstant.USER_ID_REQUEST_HEADER, userId.toString());
        }
        if(!StringUtils.isEmpty(tempId)){
            //临时id透传
            reqMutate.header(RedisConstant.TEMP_USER_ID_HEADER,tempId);
        }
        ServerHttpRequest req = reqMutate.build();


        //2、使用新的exchange，包装新请求，并往下放行
        ServerWebExchange webExchange = exchange.mutate()
                .request(req)
                .response(exchange.getResponse())
                .build();

        //3、把新的请求往下放 【新req，和原response 构成的exchange】
        return chain.filter(webExchange);
    }


//测试响应式编程
//    public static void main(String[] args) {
//        Flux<Integer> just = Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9);
//
//        //Consumer<? super T> consumer
//        just.subscribe((t)->{
//            System.out.println("我收到的数据是："+t);
//        });
//
//
//        just.subscribe((t)->{
//            System.out.println("哈哈："+t);
//        });
//
//
//        just.subscribe((t)->{
//            System.out.println("呵呵："+t);
//        });
//
//    }
    /**
     * 响应指定的对象json数据
     * @param exchange
     * @param result
     * @return
     */
    private Mono<Void> responseJson(ServerWebExchange exchange, Result<String> result) {
        //1、拿到响应
        ServerHttpResponse response = exchange.getResponse();
        String json = "{}";
        try {
           json = objectMapper.writeValueAsString(result); //转化需要写出去的对象

        } catch (JsonProcessingException e) {

        }


        //响应结束
        //2、构建需要响应出去的数据;  DataBuffer: data数据缓冲区
        Publisher<? extends Publisher<? extends DataBuffer>> body = null;

        //3、由DataBufferFactory创建出我们数据的DataBuffer
        DataBuffer buffer = new DefaultDataBufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));

        //4、把这个 buffer 用发布者发布出去
        Mono<DataBuffer> mono = Mono.just(buffer);

        //5、设置响应的数据类型和编码方式
        response.getHeaders().setContentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE));

        return response.writeWith(mono);
    }

    /**
     * 跳转到指定页面
     * @param exchange
     * @param s
     * @return
     */
    private Mono<Void> locationToPage(ServerWebExchange exchange, String s) {
        //拒绝访问,调到错误页，
        ServerHttpResponse response = exchange.getResponse();
        //1、302 重定向
        response.setStatusCode(HttpStatus.FOUND);
        //2、命令浏览器重定向到指定位置
        response.getHeaders().setLocation(URI.create(s));

        // maxAge:  10  代表10s以后浏览器自行删除这个cookie
        // maxAge:   0  代表这个cookie立即需要被浏览器删除
        // maxAge:  <0  代表这个cookie，浏览器关闭的时候就把这个删除（会话期有效[session]）
        //3、以前登录的假令牌移除掉。
        //给浏览器发一个同名cookie，并且告诉浏览器这个cookie立即删除;
        ResponseCookie token = ResponseCookie.from("token", "").maxAge(0).domain(".gmall.com").build();
        response.getCookies().add("token",token);


        ResponseCookie userInfo = ResponseCookie.from("userInfo", "").maxAge(0).domain(".gmall.com").build();
        response.getCookies().add("userInfo",userInfo);
        //4、此次响应结束
        return response.setComplete();
    }

    /**
     * 获取当前请求前端带来的token
     *
     * @param request
     * @return
     */
    private String getToken(ServerHttpRequest request) {
        String tokenCookie = getCookieValue(request, "token");
        String tokenHeader = getHeaderValue(request, "token");

        return StringUtils.isEmpty(tokenCookie) ? tokenHeader : tokenCookie;
    }


    /**
     * 获取某个cookie的值
     *
     * @param request
     * @param cookieName
     * @return
     */
    private String getCookieValue(ServerHttpRequest request, String cookieName) {
        //1、拿到当前请求所有cookie；
        MultiValueMap<String, HttpCookie> cookies = request.getCookies();
        if (cookies != null) {
            //2、拿到指定cookie
            HttpCookie first = cookies.getFirst(cookieName);
            if (first != null) {
                //3、拿到值
                String value = first.getValue();
                return value;
            }

        }

        return null; //解析不到返回null

    }

    ;

    /**
     * 获取某个请求头的值
     *
     * @param request
     * @param headerName
     * @return
     */
    public String getHeaderValue(ServerHttpRequest request, String headerName) {

        HttpHeaders headers = request.getHeaders();
        if (headers.containsKey(headerName)) {
            //如果有这个头
            String first = headers.getFirst(headerName);
            return first;
        }
        return null;
    }


}
